今天要來介紹一個非常實用的功能: 建立 Custom Scalar Type 。
前面有提到 GraphQL 預設總共有 5 種 Scalar Type ,分別為 Int, Float, String, Boolean, ID 。
GraphQL 的一大強處就是 type validation ,但當功能需求越開越多時,開始會存取 url 、 date 、 positive integer 等等特定格式的資料,只使用 String 或 Int 做 type validation 的效益不高,有跟沒有一樣。
再者,當使用你 Server 的 Clinet 種類變多 (IOS, Android, PC, MAC,...) ,寬鬆的 type validation 不但會增加存錯資料的風險也會提高 Server 檢查參數的複雜度,於是漸漸地你會發現這五種預設 Scalar Typ 似乎不太夠用,你需要自定義的 Scalar Type 來幫助你達到以下三點:
接下來就讓我為大家介紹一下如何快速上手創造自己的 Scalar Type 吧!
Date
Scalar Type接著就來實作一個 Date
Scalar Type ,我們希望的行為有
Date
Scalar Type 時需要用 Unix Epoch timestamp in milliseconds (毫秒)。Date
Scalar Type 的參數時要轉成 JS 的 Date
物件再給 Resolver 處理。Date
Scalar Type 的資料要轉成 Unix Epoch timestamp in milliseconds (毫秒) 。PS. Unix Epoch timestamp in milliseconds 是從 1970-01-01 00:00 到現在的毫秒數,可參考此網站得知目前時間 : EpochConverter
實作一個 Custom Scalar Type 需要兩部分,分別為 Schema 部分與 Resolver 部分。
Date
Scalar Type - Schema 宣告部分Schema 部分相當單純,直接宣告後就可以在其他 Object Type 或是 Input Object Type 中使用。
"""
日期格式。顯示時以 Unix Timestamp in Milliseconds 呈現。
"""
scalar Date
# 宣告後就可以在底下直接使用
type Query {
# 獲取現在時間
now: Date
# 詢問日期是否為週五... TGIF!!
isFriday(date: Date!): Boolean
}
Date
Scalar Type - Resolvers 實作有了 Schema 宣告,按照慣例也需要 Resolver 去實作 Scalar Type 的內容!不過 Custom Scalar Type 的 Resolver 實作比較複雜一點,接著我會詳細講解每個部分 (底下我也會講如何直接 import 別人寫好的 Resolver 省去維護的麻煩 XD)
首先要先 import 進來兩個工具,分別為
const { GraphQLScalarType } = require('graphql')
GraphQLScalarType
是一個用來建造新的 Scalar Type 的 classconst { Kind } = require('graphql/language')
parseLiteral
function 會用 Kind
來檢查 Type 是否合乎需求接著要在 Resolver function 中定義 Custom Scalar Type 的實作方式,而新的 GraphQLScalarType 初始化時需要以下參數:
const resolvers = {
Date: new GraphQLScalarType({
name: '',
description: '',
serialize(value) {
// value sent to the client
return '';
},
parseValue(value) {
// value from the client (variables)
return '';
},
parseLiteral(ast) {
// value from the client (inline arguments)
switch(ast.kind) {}
return '';
}
})
}
name
(Required) Scalar Type 名稱 (需對上 schema 定義時的名稱)description
(Optional) Scalar Type 介紹serialize(value)
(Required) Server 回覆給 Client 的值。value
傳進來,而 serialize
決定最後輸出的值。parseValue(value)
(Required) Client 傳給 Server 的值, value
會從 variables 中獲得。parseLiteral(ast)
(Required) Client 傳給 Server 的值, ast
會從 query 字串中解析出來,而 ast
的值是一個 AST 格式的 Object,舉個例子如下:{
kind:"IntValue" // 輸入參數的型別
loc: {start: 84, end: 97, startToken: Tok, …} // 在 query 中的位址
value:"1540791381379" // 輸入參數的值 (皆為 string 格式)
}
所以定義一個 Date
的 Resolver Fucntion 會像是這樣:
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
const resolvers = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
serialize(value) {
// 輸出到前端
// 回傳 unix timestamp 值
return value.getTime();
},
parseValue(value) {
// 從前端 variables 進來的 input
// 回傳 Date Object 到 Resolver
return new Date(value);
},
parseLiteral(ast) {
// 從前端 query 字串進來的 input
// 這邊僅接受輸入進來的是 Int 值
if (ast.kind === Kind.INT) {
// 回傳 Date Object 到 Resolver (記得要先 parseInt)
return new Date(parseInt(ast.value, 10)); // ast value is always in string format
}
return null;
}
}),
}
這邊可能還是有人很困惑為什麼 parseLiteral
傳入的是 ast
,因為當 Client 傳來 query 時, Server 只接收到一個純字串,所以需要靠 GraphQL 去 parse 這個字串成一個 AST Object (Abstract Syntax Tree) 格式供程式去解析 query 的內容。
因此如果 query 是以
query {
isFriday(date: 1540791381379)
}
來傳入,那麼裡面的 date
參數也會跟著被寫進 AST 中,進而觸發 parseLiteral
,然後我們再從 ast
中取出值來。所以一個 ast
物件會有 kind
(輸入參數型別)、loc
(query 中的位址)、 value
(輸入參數的值 - string 格式)
而如果 query 是搭配 varialbes 方式輸入:
query ($date: Date!) {
isFriday(date: $date)
}
---
VARIABLES
{
"date": 1540791381379
}
那 date
參數就會隨著 varialbes 以 JSON 格式傳進 GraphQL Server,進而觸發 parseValue
。
最後來看整個程式碼長怎樣:
Date
Scalar Type - 完整程式const { ApolloServer, gql } = require('apollo-server');
const { GraphQLScalarType } = require('graphql');
const { Kind } = require('graphql/language');
const typeDefs = gql`
scalar Date
type Query {
# 獲取現在時間
now: Date
# 詢問日期是否為週五... TGIF!!
isFriday(date: Date!): Boolean
}
`;
const resolvers = {
Date: new GraphQLScalarType({
name: 'Date',
description: 'Date custom scalar type',
serialize(value) {
// value sent to the client
// 輸出到前端
return value.getTime();
},
parseValue(value) {
// value from the client (variables)
// 從前端 variables 進來的 input
return new Date(value);
},
parseLiteral(ast) {
// value from the client (inline)
// 從前端 inline variables 進來的 input
if (ast.kind === Kind.INT) {
return new Date(parseInt(ast.value, 10)); // ast value is always in string format
}
return null;
}
}),
Query: {
now: () => new Date(),
isFriday: (root, { date }) => date.getDay() === 5
}
};
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => {
console.log(`? Server ready at ${url}`);
});
Date
Scalar Type - Demo說了這麼多,終於要 DEMO 了!
先取得現在的 unix timestamp: 1540791381379 (2018-10-29 5:36 AM +0) 再輸入 query:
query ($date: Date!) {
now
parseValueDemo: isFriday(date: 1540791381379)
parseLiteralDemo:isFriday(date: $date)
}
---
VARIABLES
{
"date": 1540791381379
}
結果如圖:
可以自己在 parseValue
與 parseLiteral
中加入 console.log
來檢查看看背後的機制是否如上所說 ~
另外可以找一下上週五中午十二點的時間:1540555200000 帶進去看看 isFriday
是否正確。
如圖:
但是如果 Unix Timestamp 輸入的是 Stirng 的話,因為目前沒有特別處理 String 的狀況,所以會變成如下圖的結果。
new Date(timestamp)
的 timestamp
為 String 格式會出錯。ast.kind
為 Kind.STRING
非 Kind.INT
,所以會送出 null 值導致之後的 Error。自行定義雖然較為麻煩,但好處是控制度高,如果有特殊需求的朋友可以考慮。
不過要自己維護也是蠻麻煩的,用一些線上的套件可以一次使用大量已經寫好的 Custom Scalar Type Resolver 。
這邊推薦 @okgrow/graphql-scalars ,裡面定義很多實用的 Resolver Functions ,只需要 import 進來放進 resolvers 中就行了!
Schema 部分一樣也是要先宣告
scalar DateTime
type Query {
# 獲取現在時間
now: DateTime
# 詢問日期是否為週五... TGIF!!
isFriday(date: DateTime!): Boolean
}
const { DateTime } = require('@okgrow/graphql-scalars');
const resolvers = {
...,
DateTime,
...
}
就完成了 !
這邊的 DateTime
在 Resolver 時一樣給你 Date
Object ,但在 Client 的 query 輸入及 Server 的 response 時會是 ISO 格式,可參考下圖。
可以輸入 query 來測試
query ($date: DateTime!) {
now
parseLiteralDemo: isFriday(date: "2018-10-26T10:10:10.000Z")
parseValueDemo:isFriday(date: $date)
}
---
VARIABLES
{
"date": "2018-10-10T10:10:10.000Z"
}
結果如圖所示:
裡面還有其他常見的 URL, Eamil, USCurrency, JSON 等等格式,可以自己來嘗試!
那麼什麼時候我們需要 Custom Scalar Type 。
可以參考 Shopify 的建議是:
而我的經驗是,一開始都先使用 String ,只有當對於特殊語意的 Scalar Type 的需求夠多時,比如每個 Business Model 都要有個 createdAt
,那我就會考慮使用為 createdAt
建立一個 Date
Scalar Type 。
不然有時候 Custom Scalar Type 也是把雙面刃,用錯時機點可能造成 Schema 規範過多不夠彈性。
Reference :
query ($date: Date!) {
now
parseValueDemo: isFriday(date: 1540791381379)
parseLiteralDemo:isFriday(date: $date)
}
兩個好像反了